/*
 *    Copyright 2022 The DSMS Authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.dsms.modules.node.task;

import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.dsms.common.constant.*;
import com.dsms.common.remotecall.model.RemoteResponse;
import com.dsms.common.taskmanager.TaskException;
import com.dsms.common.taskmanager.TaskStrategy;
import com.dsms.common.taskmanager.model.Step;
import com.dsms.common.taskmanager.model.Task;
import com.dsms.common.taskmanager.service.IStepService;
import com.dsms.common.taskmanager.service.ITaskService;
import com.dsms.dfsbroker.node.api.NodeApi;
import com.dsms.dfsbroker.node.request.PurgeOsdRequest;
import com.dsms.dfsbroker.node.request.StopOsdRequest;
import com.dsms.dfsbroker.node.request.ZapOsdRequest;
import com.dsms.modules.alert.service.IAlertAppriseService;
import com.dsms.modules.node.model.dto.NodeDeviceManageDto;
import com.dsms.modules.util.RemoteCallUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;

import static org.apache.commons.lang3.ThreadUtils.sleep;


@Slf4j
@Service(TaskTypeEnum.TypeConstants.REMOVE_OSD)
public class RemoveOsdTask implements TaskStrategy {

    private final NodeApi nodeApi;
    private final ITaskService taskService;
    private final IStepService stepService;
    private final IAlertAppriseService alertAppriseService;

    @Autowired
    public RemoveOsdTask(NodeApi nodeApi, ITaskService taskService, IStepService stepService, IAlertAppriseService alertAppriseService) {
        this.nodeApi = nodeApi;
        this.taskService = taskService;
        this.stepService = stepService;
        this.alertAppriseService = alertAppriseService;
    }

    @Override
    public Task execute(Task task) {
        String taskParam = task.getTaskParam();
        if (ObjectUtils.isEmpty(taskParam)) {
            task.setTaskEndTime(LocalDateTime.now());
            task.setTaskStatus(TaskStatusEnum.FAIL.getStatus());
            return task;
        }
        NodeDeviceManageDto nodeDeviceManageDto = JSONUtil.toBean(taskParam, NodeDeviceManageDto.class);
        int osdId = nodeDeviceManageDto.getOsdId();
        Integer taskId = task.getId();
        List<Step> steps = stepService.list(new LambdaQueryWrapper<Step>().eq(Step::getTaskId, taskId));
        try {
            //1.stop the osd
            Step stopStep = stopOsd(osdId,
                    steps.stream().filter(s -> s.getStepType().equals(StepTypeEnum.STOP_OSD.getType())).findFirst().orElse(new Step()));
            //2.purge the osd
            Step purgeStep = purgeOsd(osdId,
                    steps.stream().filter(s -> s.getStepType().equals(StepTypeEnum.PURGE_OSD.getType())).findFirst().orElse(new Step()));
            //3.zap the device of osd
            Step zapStep = zapOsd(nodeDeviceManageDto,
                    steps.stream().filter(s -> s.getStepType().equals(StepTypeEnum.ZAP_OSD.getType())).findFirst().orElse(new Step()), task.getTaskName());
        } catch (Exception e) {
            log.error("remove osd task fail, fail reason:{}", e.getMessage(), e);
            if (ObjectUtils.isEmpty(task.getTaskErrorMessage())) {
                task.setTaskErrorMessage(e.getMessage());
            }
            task.setTaskEndTime(LocalDateTime.now());
            task.setTaskStatus(TaskStatusEnum.FAIL.getStatus());
            return task;
        }
        task.setTaskEndTime(LocalDateTime.now());
        task.setTaskStatus(TaskStatusEnum.FINISH.getStatus());

        return task;
    }

    public Step stopOsd(int osdId, Step step) {
        //update step status to executing
        step.setStepStatus(TaskStatusEnum.EXECUTING.getStatus());
        try {
            RemoteResponse stopOsdResponse = nodeApi.stopOsd(RemoteCallUtil.generateRemoteRequest(), osdId);
            boolean flag = true;
            while (flag) {
                String successMsg1 = String.format(StopOsdRequest.OSD_STOP_SUCCESS_RESULT, osdId);
                String successMsg2 = String.format(StopOsdRequest.OSD_ALREADY_STOP_SUCCESS_RESULT, osdId);

                List<String> successMsg = List.of(successMsg1, successMsg2);
                step = stepService.getStepResponse(step, stopOsdResponse, successMsg, "could not stop osd");
                if (Objects.equals(step.getStepStatus(), TaskStatusEnum.FINISH.getStatus()) || Objects.equals(step.getStepStatus(), TaskStatusEnum.FAIL.getStatus())) {
                    flag = false;
                }
            }
        } catch (Throwable e) {
            step.setStepStatus(TaskStatusEnum.FAIL.getStatus());
            step.setStepEndTime(LocalDateTime.now());
            if (ObjectUtils.isEmpty(step.getStepErrorMessage())) {
                step.setStepErrorMessage(e.getMessage());
            }
            throw new TaskException(step.getStepErrorMessage());
        } finally {
            step.setStepEndTime(LocalDateTime.now());
            stepService.updateById(step);
        }
        return step;
    }

    public Step purgeOsd(int osdId, Step step) {
        //update step status to executing
        step.setStepStatus(TaskStatusEnum.EXECUTING.getStatus());
        try {
            RemoteResponse stopOsdResponse = nodeApi.purgeOsd(RemoteCallUtil.generateRemoteRequest(), osdId);
            boolean flag = true;
            while (flag) {
                String successMsg1 = String.format(PurgeOsdRequest.PURGE_SUCCESS_RESULT, osdId);
                String successMsg2 = String.format(PurgeOsdRequest.OSD_NOT_EXIST_SUCCESS_RESULT, osdId);
                List<String> successMsg = List.of(successMsg1, successMsg2);
                step = stepService.getStepResponse(step, stopOsdResponse, successMsg, "could not purge osd");
                if (Objects.equals(step.getStepStatus(), TaskStatusEnum.FINISH.getStatus()) || Objects.equals(step.getStepStatus(), TaskStatusEnum.FAIL.getStatus())) {
                    flag = false;
                }
            }
        } catch (Throwable e) {
            step.setStepStatus(TaskStatusEnum.FAIL.getStatus());
            step.setStepEndTime(LocalDateTime.now());
            if (ObjectUtils.isEmpty(step.getStepErrorMessage())) {
                step.setStepErrorMessage(e.getMessage());
            }
            throw new TaskException(step.getStepErrorMessage());
        } finally {
            step.setStepEndTime(LocalDateTime.now());
            stepService.updateById(step);
        }
        return step;
    }

    /*
     * zap the osd after remove it from cluster
     * this function would execute at most 3 times for waiting the device not busy
     * */
    @SneakyThrows
    public Step zapOsd(NodeDeviceManageDto nodeDeviceManageDto, Step step, String taskName) {
        int osdId = nodeDeviceManageDto.getOsdId();
        String host = nodeDeviceManageDto.getHost();
        String path = nodeDeviceManageDto.getPath();
        for (int i = 1; i <= ZapOsdRequest.RETRY_TIMES; i++) {
            sleep(Duration.ofSeconds(ZapOsdRequest.RETRY_TIME_SECONDS));
            step.setStepStatus(TaskStatusEnum.EXECUTING.getStatus());
            try {
                RemoteResponse zapOsdResponse = nodeApi.zapOsd(RemoteCallUtil.generateRemoteRequest(), host, path);
                boolean flag = true;
                while (flag) {
                    List<String> successMsg = List.of(String.format(ZapOsdRequest.ZAP_SUCCESSFUL_RESULT, path));
                    step = stepService.getStepResponse(step, zapOsdResponse, successMsg, "could not zap osd");
                    if (Objects.equals(step.getStepStatus(), TaskStatusEnum.FINISH.getStatus()) || Objects.equals(step.getStepStatus(), TaskStatusEnum.FAIL.getStatus())) {
                        flag = false;
                    }
                }
                if (Objects.equals(step.getStepStatus(), TaskStatusEnum.FINISH.getStatus())) {
                    break;
                }
            } catch (Throwable e) {
                log.warn("zap osd failed,start retry,retry times:{}", i);
                if (i == ZapOsdRequest.RETRY_TIMES) {
                    step.setStepStatus(TaskStatusEnum.FAIL.getStatus());
                    step.setStepEndTime(LocalDateTime.now());
                    if (ObjectUtils.isEmpty(step.getStepErrorMessage())) {
                        step.setStepErrorMessage(e.getMessage());
                    }
                    String message = taskName + "失败:无法擦除" + host + ":osd." + osdId + ",盘符:" + path;
                    alertAppriseService.notifyContact(message, AlertRuleModuleEnum.NODE, AlertRuleLevelEnum.ERROR);
                    throw new TaskException(step.getStepErrorMessage());
                }
            } finally {
                step.setStepEndTime(LocalDateTime.now());
                stepService.updateById(step);
            }
        }
        return step;
    }

    @Override
    public boolean validateTask(String[] validateParam) {
        LambdaQueryWrapper<Task> taskQuery = new LambdaQueryWrapper<>();
        taskQuery.in(Task::getTaskStatus, TaskStatusEnum.QUEUE.getStatus(), TaskStatusEnum.EXECUTING.getStatus())
                .eq(Task::getTaskType, TaskTypeEnum.REMOVE_OSD.getType())
                .eq(Task::getTaskParam, validateParam[0]);
        long count = taskService.count(taskQuery);

        return count <= 0;
    }

}
